/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Copyright (C) 2014 - 2016 Red Hat, Inc.
 */

#include "src/core/nm-default-daemon.h"

#include "nm-dhcp-listener.h"

#include <sys/socket.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

#include "nm-dhcp-helper-api.h"
#include "nm-dhcp-client.h"
#include "nm-dhcp-manager.h"
#include "libnm-core-intern/nm-core-internal.h"
#include "nm-dbus-manager.h"
#include "NetworkManagerUtils.h"

#define PRIV_SOCK_PATH NMRUNDIR "/private-dhcp"
#define PRIV_SOCK_TAG  "dhcp"

/*****************************************************************************/

const NMDhcpClientFactory *const _nm_dhcp_manager_factories[6] = {

    /* the order here matters, as we will try the plugins in this order to find
     * the first available plugin. */

    &_nm_dhcp_client_factory_internal,
#if WITH_DHCPCANON
    &_nm_dhcp_client_factory_dhcpcanon,
#endif
#if WITH_DHCPCD
    &_nm_dhcp_client_factory_dhcpcd,
#endif
    &_nm_dhcp_client_factory_systemd,
    &_nm_dhcp_client_factory_nettools,
#if WITH_DHCLIENT
    &_nm_dhcp_client_factory_dhclient,
#endif
};

/*****************************************************************************/

typedef struct {
    NMDBusManager *dbus_mgr;
    gulong         new_conn_id;
    gulong         dis_conn_id;
    GHashTable    *connections;
} NMDhcpListenerPrivate;

struct _NMDhcpListener {
    GObject               parent;
    NMDhcpListenerPrivate _priv;
};

struct _NMDhcpListenerClass {
    GObjectClass parent;
};

enum { EVENT, LAST_SIGNAL };
static guint signals[LAST_SIGNAL] = {0};

G_DEFINE_TYPE(NMDhcpListener, nm_dhcp_listener, G_TYPE_OBJECT)

#define NM_DHCP_LISTENER_GET_PRIVATE(self) \
    _NM_GET_PRIVATE(self, NMDhcpListener, NM_IS_DHCP_LISTENER)

NM_DEFINE_SINGLETON_GETTER(NMDhcpListener, nm_dhcp_listener_get, NM_TYPE_DHCP_LISTENER);

/*****************************************************************************/

#define _NMLOG_PREFIX_NAME "dhcp-listener"
#define _NMLOG_DOMAIN      LOGD_DHCP
#define _NMLOG(level, ...)                                                         \
    G_STMT_START                                                                   \
    {                                                                              \
        const NMDhcpListener *_self = (self);                                      \
        char                  _prefix[64];                                         \
                                                                                   \
        nm_log((level),                                                            \
               (_NMLOG_DOMAIN),                                                    \
               NULL,                                                               \
               NULL,                                                               \
               "%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                          \
               (_self != singleton_instance                                        \
                    ? nm_sprintf_buf(_prefix, "%s[%p]", _NMLOG_PREFIX_NAME, _self) \
                    : _NMLOG_PREFIX_NAME) _NM_UTILS_MACRO_REST(__VA_ARGS__));      \
    }                                                                              \
    G_STMT_END

/*****************************************************************************/

static char *
get_option(GVariant *options, const char *key)
{
    GVariant     *value;
    const guchar *bytes, *s;
    gsize         len;
    char         *converted, *d;

    if (!g_variant_lookup(options, key, "@ay", &value))
        return NULL;

    bytes = g_variant_get_fixed_array(value, &len, 1);

    /* Since the DHCP options come through environment variables, they should
     * already be UTF-8 safe, but just make sure.
     */
    converted = g_malloc(len + 1);
    for (s = bytes, d = converted; s < bytes + len; s++, d++) {
        /* Convert NULLs to spaces and non-ASCII characters to ? */
        if (*s == '\0')
            *d = ' ';
        else if (*s > 127)
            *d = '?';
        else
            *d = *s;
    }
    *d = '\0';
    g_variant_unref(value);

    return converted;
}

static void
_method_call_handle(NMDhcpListener *self, GDBusMethodInvocation *invocation, GVariant *parameters)
{
    gs_free char              *iface   = NULL;
    gs_free char              *pid_str = NULL;
    gs_free char              *reason  = NULL;
    gs_unref_variant GVariant *options = NULL;
    int                        pid;
    gboolean                   handled = FALSE;

    g_variant_get(parameters, "(@a{sv})", &options);

    iface = get_option(options, "interface");
    if (iface == NULL) {
        _LOGW("dhcp-event: didn't have associated interface.");
        goto out;
    }

    pid_str = get_option(options, "pid");
    pid     = _nm_utils_ascii_str_to_int64(pid_str, 10, 0, G_MAXINT32, -1);
    if (pid == -1) {
        _LOGW("dhcp-event: couldn't convert PID '%s' to an integer", pid_str ?: "(null)");
        goto out;
    }

    reason = get_option(options, "reason");
    if (reason == NULL) {
        _LOGW("dhcp-event: (pid %d) DHCP event didn't have a reason", pid);
        goto out;
    }

    g_signal_emit(self, signals[EVENT], 0, iface, pid, options, reason, invocation, &handled);
    if (!handled) {
        if (g_ascii_strcasecmp(reason, "RELEASE") == 0) {
            /* Ignore event when the dhcp client gets killed and we receive its last message */
            _LOGD("dhcp-event: (pid %d) unhandled RELEASE DHCP event for interface %s", pid, iface);
        } else
            _LOGW("dhcp-event: (pid %d) unhandled DHCP event for interface %s", pid, iface);
    }

out:
    if (!handled)
        g_dbus_method_invocation_return_value(invocation, NULL);
}

static void
_method_call(GDBusConnection       *connection,
             const char            *sender,
             const char            *object_path,
             const char            *interface_name,
             const char            *method_name,
             GVariant              *parameters,
             GDBusMethodInvocation *invocation,
             gpointer               user_data)
{
    NMDhcpListener *self = NM_DHCP_LISTENER(user_data);

    if (!nm_streq(interface_name, NM_DHCP_HELPER_SERVER_INTERFACE_NAME)
        || !nm_streq(method_name, NM_DHCP_HELPER_SERVER_METHOD_NOTIFY)) {
        g_dbus_method_invocation_return_error(invocation,
                                              G_DBUS_ERROR,
                                              G_DBUS_ERROR_UNKNOWN_METHOD,
                                              "Unknown method %s",
                                              method_name);
        return;
    }

    _method_call_handle(self, invocation, parameters);
}

static GDBusInterfaceInfo *const interface_info = NM_DEFINE_GDBUS_INTERFACE_INFO(
    NM_DHCP_HELPER_SERVER_INTERFACE_NAME,
    .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
        NM_DEFINE_GDBUS_METHOD_INFO(NM_DHCP_HELPER_SERVER_METHOD_NOTIFY,
                                    .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
                                        NM_DEFINE_GDBUS_ARG_INFO("data", "a{sv}"), ), ), ), );

static guint
_dbus_connection_register_object(NMDhcpListener *self, GDBusConnection *connection, GError **error)
{
    static const GDBusInterfaceVTable interface_vtable = {
        .method_call = _method_call,
    };

    return g_dbus_connection_register_object(
        connection,
        NM_DHCP_HELPER_SERVER_OBJECT_PATH,
        interface_info,
        NM_UNCONST_PTR(GDBusInterfaceVTable, &interface_vtable),
        self,
        NULL,
        error);
}

static void
new_connection_cb(NMDBusManager      *mgr,
                  GDBusConnection    *connection,
                  GDBusObjectManager *manager,
                  NMDhcpListener     *self)
{
    NMDhcpListenerPrivate *priv = NM_DHCP_LISTENER_GET_PRIVATE(self);
    guint                  registration_id;
    GError                *error = NULL;

    /* it is important to register the object during the new-connection signal,
     * as this avoids races with the connecting object. */
    registration_id = _dbus_connection_register_object(self, connection, &error);
    if (!registration_id) {
        _LOGE("failure to register %s for connection %p: %s",
              NM_DHCP_HELPER_SERVER_OBJECT_PATH,
              connection,
              error->message);
        g_error_free(error);
        return;
    }

    g_hash_table_insert(priv->connections, connection, GUINT_TO_POINTER(registration_id));
}

static void
dis_connection_cb(NMDBusManager *mgr, GDBusConnection *connection, NMDhcpListener *self)
{
    NMDhcpListenerPrivate *priv = NM_DHCP_LISTENER_GET_PRIVATE(self);
    guint                  id;

    id = GPOINTER_TO_UINT(g_hash_table_lookup(priv->connections, connection));
    if (id) {
        g_dbus_connection_unregister_object(connection, id);
        g_hash_table_remove(priv->connections, connection);
    }
}

/*****************************************************************************/

static void
nm_dhcp_listener_init(NMDhcpListener *self)
{
    NMDhcpListenerPrivate *priv = NM_DHCP_LISTENER_GET_PRIVATE(self);

    /* Maps GDBusConnection :: signal-id */
    priv->connections = g_hash_table_new(nm_direct_hash, NULL);

    priv->dbus_mgr = g_object_ref(nm_dbus_manager_get());

    /* Register the socket our DHCP clients will return lease info on */
    nm_dbus_manager_private_server_register(priv->dbus_mgr, PRIV_SOCK_PATH, PRIV_SOCK_TAG);
    priv->new_conn_id = g_signal_connect(priv->dbus_mgr,
                                         NM_DBUS_MANAGER_PRIVATE_CONNECTION_NEW "::" PRIV_SOCK_TAG,
                                         G_CALLBACK(new_connection_cb),
                                         self);
    priv->dis_conn_id =
        g_signal_connect(priv->dbus_mgr,
                         NM_DBUS_MANAGER_PRIVATE_CONNECTION_DISCONNECTED "::" PRIV_SOCK_TAG,
                         G_CALLBACK(dis_connection_cb),
                         self);
}

static void
dispose(GObject *object)
{
    NMDhcpListenerPrivate *priv = NM_DHCP_LISTENER_GET_PRIVATE(object);

    nm_clear_g_signal_handler(priv->dbus_mgr, &priv->new_conn_id);
    nm_clear_g_signal_handler(priv->dbus_mgr, &priv->dis_conn_id);

    nm_clear_pointer(&priv->connections, g_hash_table_destroy);

    g_clear_object(&priv->dbus_mgr);

    G_OBJECT_CLASS(nm_dhcp_listener_parent_class)->dispose(object);
}

static void
nm_dhcp_listener_class_init(NMDhcpListenerClass *listener_class)
{
    GObjectClass *object_class = G_OBJECT_CLASS(listener_class);

    object_class->dispose = dispose;

    signals[EVENT] = g_signal_new(NM_DHCP_LISTENER_EVENT,
                                  G_OBJECT_CLASS_TYPE(object_class),
                                  G_SIGNAL_RUN_LAST,
                                  0,
                                  g_signal_accumulator_true_handled,
                                  NULL,
                                  NULL,
                                  G_TYPE_BOOLEAN, /* listeners return TRUE if handled */
                                  5,
                                  G_TYPE_STRING,  /* iface */
                                  G_TYPE_INT,     /* pid */
                                  G_TYPE_VARIANT, /* options */
                                  G_TYPE_STRING,  /* reason */
                                  G_TYPE_DBUS_METHOD_INVOCATION /* invocation*/);
}
